実装の技術周り | ScrapboxでVim key bindingsを作ろうと試行錯誤してる話
Vimのコマンドを調べる
実装すべき操作を調べる
チートシートやヘルプファイルを見ながら一つ一つ調べた
おかげでg_とか全然聞いたこともないコマンドを覚えたりした
{count}<C-u>が{count}kと同じコマンドになることも初めて知った
実際にVimでコマンドを打ってみて、どんな挙動をするのか確かめた
hlは行を跨がないだとか
insert modeからnormal modeに戻ったとき、カーソルがどこの文字に移動するのかだとか
実装方法
キーボード入力代行やテキストの座標計算などはVimと全然関係ない部分なので割愛します。
代わりにVim key bindからcommandを解釈する処理を実装する試行錯誤の過程を解説します。
switch
scrapVim-lite-2まで最初は動けばいいの精神で、switchを使ってkey biningsごとにコマンドを対応させていました
code:js
onNormalMode(keymap) {
this._log('Analyze commands as a one of the normal mode.');
switch (keymap) {
case 'h':
move.left({cursor: this.cursorInfo});
break;
case 'j':
move.down();
break;
case 'k':
move.up();
break;
case 'l':
move.right({cursor: this.cursorInfo});
break;
// 略
case 'i':
insert.before({onInsert: () => this._onInsert()});
break;
case 'a':
insert.after({onInsert: () => this._onInsert()});
break;
case 'I':
insert.startOfLine({onInsert: () => this._onInsert(), cursor: this.cursorInfo});
break;
case 'A':
insert.endOfLine({onInsert: () => this._onInsert()});
break;
case 'o':
insert.newLineBelow({onInsert: () => this._onInsert()});
break;
case 'O':
insert.newLineAbove({onInsert: () => this._onInsert()});
break;
case 'x':
edit.cut();
break;
case 'd':
if (this.stack.last !== 'd') return;
edit.deleteLine();
break;
case 'D':
edit.deleteForEnd();
break;
// 略
default:
break;
}
this.stack.flush();
}
はい。見るからにダメコードですね。このままではユーザがコマンドを定義できませんし、何より2文字以上のコマンドに対応するのが不可能です。
流石にこんなコードをそのまま放置するのは嫌気が差したので、key bindingsをまともに解析して対応するコマンドを決定する処理を設計することにしました。
候補1: コマンドとkey bindingsとのペアを作る
switchの回避は出来ますが柔軟なコマンドの指定が出来ないので即却下しました
ここがvim key bindingsと他のkey bindings/ keyboard shortcutとで決定的に異なる点です。
他のserviceのshortcut keyは、キーとコマンドとが一対一に対応しています。なので候補1の方法で十分です。
一方vim key bindingsは一定の文法に従って柔軟に変化させられます
dを例に取ってみると、["x][count]d{motion}のように3つの文字列をとることができ、それぞれ決まったグループの文字列のみ入れられるようになっています
全ての文字列を予め列挙しておくことが無謀である以上、一対一のペアで対応することは不可能です
候補2: 状態遷移を使う
registerやmotionなどのグループ単位なら、key bindingsと操作の一対一対応が成立しているので、そのグループ単位で解析するという案です
図式するとこんなかんじです
http://www.plantuml.com/plantuml/svg/RP112iCW44NtESMGPS4hbDoZxKBYa0ZLaN5SzFQLo5GgxlJvdFzros9PIdWlftS8699ym67UsIVn59V7xGM6_N6AkGFZuRCW_rFQgKGPM4AsGeCv4GDTCRM68AnwxHalTGMRTRncW-cHoYQvpPWSw09CILgfmx5NcpBIhbTMdxCq_jjk65tzqoy0#.svg
初期状態 (図の黒丸)はregister, 繰り返し指定, operator, motionのにいずれかに該当するkeyを受け付けます
その後例えば"が入力されたらregisterに移行し、さらにaが押されたらregister"aを確定し、operator count, operatorのいずれかに該当するkeyを受け付ける状態に遷移します
二重丸の状態にまで遷移したら、解析したコマンドを実行します
text objectは複雑になるので図から省いています。もしかしたら後で修正するかも
おそらくここまでは自然に思いつくことだと思います。ただいざ実装しようと設計を詰めていったときに、解析しているコマンドをどこに持たせるかとかでだいぶ詰まりました
最終的には状態を全て関数にして、解析中のコマンドと入力されたkeyを受け取って次の状態を表す関数を返すような形式にしました
書いていて今更ですが、既存の構文解析処理を真似て作ったほうがどう考えても早いですね……視野が狭くなっていたのか、他のprogramを参考にするという発想は思いつきませんでした。